﻿/****************************************************************************************************
	Copyright (C) 2010 RapidWebDev Organization (http://rapidwebdev.org)
	Author: Eunge, Legal Name: Jian Liu, Email: eunge.liu@RapidWebDev.org

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 2 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ****************************************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Lucene.Net.Analysis;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Store;
using RapidWebDev.Common;
using RapidWebDev.SearchIndex.Contract;

namespace RapidWebDev.SearchIndex.DocumentHandlers
{
	/// <summary>
	/// The implementation to update an existed indexing document incrementally.
	/// </summary>
	public class UpdateIndexDocumentHandler : IIndexDocumentHandler
	{
		private static readonly Analyzer defaultAnalyzer = new WhitespaceAnalyzer();
		private ILuceneAnalyzerResolver luceneAnalyzerResolver;

		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="luceneAnalyzerResolver"></param>
		public UpdateIndexDocumentHandler(ILuceneAnalyzerResolver luceneAnalyzerResolver)
		{
			this.luceneAnalyzerResolver = luceneAnalyzerResolver;
		}

		/// <summary>
		/// Process index document for the index directory path.
		/// </summary>
		/// <param name="indexDirectoryPath"></param>
		/// <param name="indexDocMessage"></param>
		public void Process(string indexDirectoryPath, IndexDocMessage indexDocMessage)
		{
			try
			{
				System.IO.DirectoryInfo directoryInfo = null;
				if (!System.IO.Directory.Exists(indexDirectoryPath))
					directoryInfo = System.IO.Directory.CreateDirectory(indexDirectoryPath);
				else
					directoryInfo = new System.IO.DirectoryInfo(indexDirectoryPath);

				FSDirectory directory = FSDirectory.Open(directoryInfo);
				try
				{
					Analyzer analyzer = this.ResolveIndexWriterAnalyzer(indexDocMessage);
					IndexWriter indexWriter = new IndexWriter(directory, analyzer, IndexWriter.MaxFieldLength.UNLIMITED);
					try
					{
						UpdateLuceneDocument(indexWriter, indexDocMessage);
					}
					finally
					{
						if (indexWriter != null)
							indexWriter.Close();
					}
				}
				finally
				{
					if (directory != null)
						directory.Close();
				}
			}
			catch (Exception exp)
			{
				Logger.Instance(this).Error(exp);
				throw;
			}
		}

		private Analyzer ResolveIndexWriterAnalyzer(IndexDocMessage indexDocMessage)
		{
			Dictionary<string, Analyzer> analyzerByFields = new Dictionary<string, Analyzer>();
			analyzerByFields[LuceneFields.Content] = this.luceneAnalyzerResolver.Resolve(IndexDocFieldValueTypes.Text, indexDocMessage.StemmingLanguage);

			if (indexDocMessage.Fields != null && indexDocMessage.Fields.Count > 0)
			{
				foreach (IndexDocFieldMessage indexDocFieldMessage in indexDocMessage.Fields)
				{
					Analyzer analyzer = this.luceneAnalyzerResolver.Resolve(indexDocFieldMessage.Type, indexDocMessage.StemmingLanguage);
					if (analyzer != null)
						analyzerByFields[indexDocFieldMessage.Name] = analyzer;
				}
			}

			if (indexDocMessage.Attachments != null && indexDocMessage.Attachments.Count > 0)
			{
				for (int i = 0; i < indexDocMessage.Attachments.Count; i++)
					analyzerByFields[LuceneFields.AttachmentNameField(i)] = this.luceneAnalyzerResolver.Resolve(IndexDocFieldValueTypes.Text, indexDocMessage.StemmingLanguage);
			}

			PerFieldAnalyzerWrapper perFieldAnalyzerWrapper = new PerFieldAnalyzerWrapper(defaultAnalyzer, analyzerByFields);
			return perFieldAnalyzerWrapper;
		}

		private static void UpdateLuceneDocument(IndexWriter indexWriter, IndexDocMessage indexDocMessage)
		{
			Query queryToSearchExistedDocuments = GetLuceneDocumentQueryById(indexDocMessage.Type, indexDocMessage.DocumentId);

			Document docToUpdate = null;
			IndexSearcher searcher = new IndexSearcher(indexWriter.GetDirectory(), true);
			try
			{
				Sort sort = new Sort(new SortField(LuceneFields.UpdatedOn, SortField.STRING, true));
				TopFieldDocs topFieldDocs = searcher.Search(queryToSearchExistedDocuments, null, 1, sort);

				// there is no existing doc with document type and id.
				if (topFieldDocs.totalHits <= 0)
					throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "The index doc (DocumentType={0}, Id={1}) doesn't not exist in index.", indexDocMessage.Type, indexDocMessage.DocumentId));

				docToUpdate = searcher.Doc(topFieldDocs.scoreDocs[0].doc);
			}
			finally
			{
				if (searcher != null)
					searcher.Close();
			}

			Document newDocToAdd = UpdateLuceneDocumentIncrementally(docToUpdate, indexDocMessage);

			indexWriter.DeleteDocuments(queryToSearchExistedDocuments);
			indexWriter.AddDocument(newDocToAdd);
			indexWriter.Commit();
		}

		private static Query GetLuceneDocumentQueryById(string documentType, string documentId)
		{
			BooleanQuery queryToRun = new BooleanQuery();
			Term documentIdTerm = new Term(LuceneFields.DocumentId, documentId);
			queryToRun.Add(new TermQuery(documentIdTerm), BooleanClause.Occur.MUST);

			Term documentTypeTerm = new Term(LuceneFields.DocumentType, documentType);
			queryToRun.Add(new TermQuery(documentTypeTerm), BooleanClause.Occur.MUST);
			return queryToRun;
		}

		/// <summary>
		/// Update fields of the old doc by index doc message incrementally.
		/// * NOT-Stored fields of the old doc will be lost in the method. This may need refactor when we have NOT-stored and Analyzed fields in index.
		/// </summary>
		/// <param name="oldDoc"></param>
		/// <param name="indexDocMessage"></param>
		/// <returns></returns>
		private static Document UpdateLuceneDocumentIncrementally(Document oldDoc, IndexDocMessage indexDocMessage)
		{
			Document newDoc = new Document();
			newDoc.Add(new Field(LuceneFields.ApplicationId, indexDocMessage.ApplicationId, Field.Store.YES, Field.Index.NOT_ANALYZED));
			newDoc.Add(new Field(LuceneFields.DocumentType, indexDocMessage.Type, Field.Store.YES, Field.Index.NOT_ANALYZED));
			newDoc.Add(new Field(LuceneFields.DocumentId, indexDocMessage.DocumentId, Field.Store.YES, Field.Index.NOT_ANALYZED));
			newDoc.Add(new Field(LuceneFields.StemmingLanguage, indexDocMessage.StemmingLanguage, Field.Store.YES, Field.Index.NOT_ANALYZED));
			newDoc.Add(new Field(LuceneFields.UpdatedOn, DateTools.DateToString(DateTime.UtcNow, DateTools.Resolution.MILLISECOND), Field.Store.YES, Field.Index.NOT_ANALYZED));

			List<string> contentFieldBuilder = new List<string>();

			// copy original fields
			CopyOriginalLuceneFields(newDoc, oldDoc, indexDocMessage, contentFieldBuilder);

			// update fields
			AddLuceneFieldsFromIndexDocMessage(newDoc, indexDocMessage, contentFieldBuilder);

			// update attachments
			UpdateAttachments(newDoc, oldDoc, indexDocMessage, contentFieldBuilder);

			// add Content field
			foreach (string contentFieldValue in contentFieldBuilder)
				newDoc.Add(new Field(LuceneFields.Content, contentFieldValue, Field.Store.YES, Field.Index.ANALYZED));

			return newDoc;
		}

		/// <summary>
		/// Copy original fields of the old doc to the new doc except common fields, attachments fields and fields which have been included in the index doc message.
		/// </summary>
		/// <param name="newDoc"></param>
		/// <param name="oldDoc"></param>
		/// <param name="indexDocMessage"></param>
		/// <param name="contentFieldBuilder"></param>
		private static void CopyOriginalLuceneFields(Document newDoc, Document oldDoc, IndexDocMessage indexDocMessage, List<string> contentFieldBuilder)
		{
			HashSet<string> indexDocMessageFields = new HashSet<string>((from f in indexDocMessage.Fields
																  select f.Name).Distinct());

			foreach (Field field in oldDoc.GetFields())
			{
				Fieldable workingField = field;
				string fieldName = field.Name();
				if (LuceneFields.IsCommonField(fieldName)) continue;
				if (LuceneFields.IsAttachmentField(fieldName)) continue;

				if (LuceneFields.IsFieldTypeField(fieldName))
				{
					string originalFieldName = LuceneFields.GetOriginalFieldNameOfFieldTypeField(fieldName);
					if (indexDocMessageFields.Contains(originalFieldName)) continue;
				}
				else if (LuceneFields.IsSortableFieldName(fieldName))
				{
					string originalFieldName = LuceneFields.GetOriginalFieldNameOfSortableField(fieldName);
					if (indexDocMessageFields.Contains(originalFieldName)) continue;
				}
				else
				{
					// if fieldName is included in index doc messages, it indicates the field should be added from the index doc message later.
					if (indexDocMessageFields.Contains(fieldName)) continue;

					// append content of Text field to content field builder.
					string fieldTypeFieldName = LuceneFields.FieldType(fieldName);
					try
					{
						string fieldTypeText = oldDoc.Get(fieldTypeFieldName);
						IndexDocFieldValueTypes fieldType = (IndexDocFieldValueTypes)Enum.Parse(typeof(IndexDocFieldValueTypes), fieldTypeText);
						if (fieldType == IndexDocFieldValueTypes.Text)
						{
							contentFieldBuilder.Add(field.StringValue());
						}
						else if (fieldType == IndexDocFieldValueTypes.Decimal)
						{
							string fieldValueText = DocumentFieldFactory.Create(IndexDocFieldValueTypes.Decimal).ResolveOriginalFieldValue(field.StringValue());
							double fieldValueDouble;
							if (double.TryParse(fieldValueText, out fieldValueDouble))
							{
								NumericField numericField = new NumericField(fieldName, Field.Store.YES, true);
								numericField.SetDoubleValue(fieldValueDouble);
								workingField = numericField;
							}
						}
						else if (fieldType == IndexDocFieldValueTypes.Integer)
						{
							string fieldValueText = DocumentFieldFactory.Create(IndexDocFieldValueTypes.Integer).ResolveOriginalFieldValue(field.StringValue());
							int fieldValueInt;
							if (int.TryParse(fieldValueText, out fieldValueInt))
							{
								NumericField numericField = new NumericField(fieldName, Field.Store.YES, true);
								numericField.SetIntValue(fieldValueInt);
								workingField = numericField;
							}
						}
					}
					catch (Exception exp)
					{
						Logger.Instance(typeof(SearchService)).Warn(exp);
						continue;
					}
				}

				newDoc.Add(workingField);
			}
		}

		/// <summary>
		/// Add fields from the index doc message to the doc.
		/// </summary>
		/// <param name="newDoc"></param>
		/// <param name="indexDocMessage"></param>
		/// <param name="contentFieldBuilder"></param>
		private static void AddLuceneFieldsFromIndexDocMessage(Document newDoc, IndexDocMessage indexDocMessage, List<string> contentFieldBuilder)
		{
			foreach (IndexDocFieldMessage indexDocField in indexDocMessage.Fields)
			{
				if (indexDocField.Values == null || indexDocField.Values.Count == 0)
					continue;

				IDocumentField documentField = DocumentFieldFactory.Create(indexDocField.Type);
				if (documentField == null) continue;

				foreach (Fieldable field in documentField.GetLuceneFields(indexDocField))
					newDoc.Add(field);

				if (indexDocField.Type == IndexDocFieldValueTypes.Text)
					foreach (string indexDocFieldValue in indexDocField.Values)
						contentFieldBuilder.Add(indexDocFieldValue);
			}
		}

		/// <summary>
		/// Update attachments of the doc which does NOT support incremental update.
		/// If clients don't want to update attachments, then resolves all attachments from the old doc to the new doc.
		/// If clients want to set attachments, then resolves all attachments from the index doc message.
		/// </summary>
		/// <param name="newDoc"></param>
		/// <param name="oldDoc"></param>
		/// <param name="indexDocMessage"></param>
		/// <param name="contentFieldBuilder"></param>
		private static void UpdateAttachments(Document newDoc, Document oldDoc, IndexDocMessage indexDocMessage, List<string> contentFieldBuilder)
		{
			// if clients don't want to update attachments, then resolves all attachments from the old doc to the new doc.
			if (indexDocMessage.Attachments == null)
			{
				foreach (Field field in oldDoc.GetFields())
				{
					string fieldName = field.Name();
					if (LuceneFields.IsAttachmentField(fieldName) || string.Equals(fieldName, LuceneFields.AttachmentsCount, StringComparison.OrdinalIgnoreCase))
					{
						newDoc.Add(field);
						if (LuceneFields.IsAttachmentNameField(fieldName))
							contentFieldBuilder.Add(field.StringValue());
					}
				}

				return;
			}

			// if clients want to set attachments, then resolves all attachments from the index doc message.
			// doesn't support incremental update.
			newDoc.Add(new Field(LuceneFields.AttachmentsCount, indexDocMessage.Attachments.Count.ToString(CultureInfo.InvariantCulture), Field.Store.YES, Field.Index.NOT_ANALYZED));
			for (int i = 0; i < indexDocMessage.Attachments.Count; i++)
			{
				IndexDocAttachmentMessage indexDocAttachment = indexDocMessage.Attachments[i];
				newDoc.Add(new Field(LuceneFields.AttachmentIdField(i), indexDocAttachment.Id, Field.Store.YES, Field.Index.NOT_ANALYZED));
				newDoc.Add(new Field(LuceneFields.AttachmentNameField(i), indexDocAttachment.Name, Field.Store.YES, Field.Index.ANALYZED));
				newDoc.Add(new Field(LuceneFields.AttachmentSharedPathField(i), indexDocAttachment.SharedPath, Field.Store.YES, Field.Index.NOT_ANALYZED));
				
				// add attachment name to Content field of the doc.
				contentFieldBuilder.Add(indexDocAttachment.Name);
			}
		}
	}
}